package com.blog.cloud.security.filter;

import cn.hutool.core.date.DateUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTValidator;
import cn.hutool.jwt.signers.JWTSignerUtil;
import com.blog.cloud.security.bo.UserInfo;
import com.blog.cloud.security.component.RedisTokenStore;
import com.blog.cloud.security.utils.JwtTokenUtil;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.util.Objects;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final RedisTokenStore redisTokenStore;

    // 加密密钥
    @Value("security.key")
    private String securityKey = "12345678901234567890123456789012";

    @Override
    @SneakyThrows
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
        // 所有访问 spring security 所保护的 url 都需要经过认证，如果没有认证，则直接返回401。即使用户已经登录了

        // 从请求头中获取token，格式 Authorization: Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        String authorizationHeader = request.getHeader("Authorization");

        String access_token = null;
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            // 提取 access_token
            access_token = authorizationHeader.substring(7);
            try {
                byte[] key = securityKey.getBytes();
                // 验证 jwt token 签名，防伪造令牌
                // 由于JWT.verify，只能验证JWT Token的签名是否有效，其他payload字段验证都可以使用JWTValidator完成。
                boolean verify = JWT.of(access_token).setSigner(JWTSignerUtil.hs256(key)).verify();
                if (!verify) {
                    logger.warn("JWT Token 签名验证失败");
                    return;
                }
                // (签发时间)---------(生效时间)---------(当前时间)---------(失效时间)
                // 签发时间和生效时间一般没有前后要求，都早于当前时间即可。
                // 由于定义了签发时间、过期时间，因此会检查签发时间是否早于当前时间，过期时间是否晚于当前时间。
                // 如果 token 过期则会抛出异常 cn.hutool.core.exceptions.ValidateException: 'exp':[2025-01-05 23:46:30] is before now:[2025-01-05 23:50:28]
                JWTValidator.of(access_token).validateDate(DateUtil.date());


                username = jwtTokenUtil.getUsernameFromToken(access_token);
            } catch (IllegalArgumentException e) {
                logger.warn("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                logger.warn("JWT Token has expired");
            }
        }

        String token = JwtTokenUtil.getToken(request);

        // 如果请求中没有认证信息则表示未登录状态
        if (StringUtils.isBlank(token)) {
            chain.doFilter(request, response);
            return;
        }

        // 如果有则开始认证过程
        UserInfo userInfo = redisTokenStore.getUserInfo(token);
        if (Objects.isNull(userInfo)) {
            chain.doFilter(request, response);
            return;
        }

        // 如果redis中有则表明登陆状态还有效，直接使用
        Authentication authentication = new UsernamePasswordAuthenticationToken(userInfo.getUsername(), userInfo.getPassword(),userInfo.getAuthorities());
        // 将认证信息放入认证容器, 后续的过滤器会根据认证信息进行权限校验
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }
}

// 如果请求头带有token认证信息，则表明可能登陆过，因此在请求时先判断token有没有过期，如果redis中查不到信息，则过期，反之有效，然后从中取出用户认证信息,直接放入认证容器
// 因此在登录存放认证信息时也要存放认证对象才行
// spring security 默认的表单登录使用的是 UsernamePasswordAuthenticationFilter 过滤器，这里面进行了认证过程，自定义的并没有这个过滤器